/******************************************************************************* * Copyright (c) 2009 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.ui.internal.dialogs; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.swt.graphics.Image; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.viewers.CheckStateChangedEvent; import org.eclipse.jface.viewers.CheckboxTableViewer; import org.eclipse.jface.viewers.CheckboxTreeViewer; import org.eclipse.jface.viewers.IBaseLabelProvider; import org.eclipse.jface.viewers.ICheckStateListener; import org.eclipse.jface.viewers.ICheckStateProvider; import org.eclipse.jface.viewers.ICheckable; import org.eclipse.jface.viewers.IContentProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.LabelProvider; import org.eclipse.jface.viewers.Viewer; /** * Manages a tree which provides "standard checkbox tree behavior". I.e. it * follows these rules: * <ol> * <li>If a check box is checked, all its children must be checked.</li> * <li>If a check box is unchecked, all its children must be unchecked.</li> * <li>If a check box is grayed, each of its children may be either checked or * unchecked, however, there must be one of each.</li> * <li>If a user checks a check box, its children or parents must change state * accordingly.</li> * </ol> * <p> * <b>Note:</b> be sure to call dispose() * </p> * @since 3.5 * */ public class TreeManager { static final int CHECKSTATE_UNCHECKED = 0; static final int CHECKSTATE_GRAY = 1; static final int CHECKSTATE_CHECKED = 2; private static ICheckStateProvider checkStateProvider = null; private static IBaseLabelProvider labelProvider = null; private static ICheckStateListener viewerCheckListener = null; private static ITreeContentProvider treeContentProvider = null; private List listeners = new ArrayList(); private LocalResourceManager resourceManager = new LocalResourceManager(JFaceResources.getResources()); /** * Instances of this interface will handle changes in the model * representation of checkstates. */ public interface CheckListener { /** * Invoked when a {@link TreeManager.TreeItem}'s check state has changed. * * @param changedItem The item whose check state has changed */ public void checkChanged(TreeItem changedItem); } /** * Implementation of {@link TreeManager.CheckListener} for use in a {@link CheckboxTreeViewer}. */ public static class ModelListenerForCheckboxTree implements CheckListener { private CheckboxTreeViewer treeViewer; public ModelListenerForCheckboxTree(TreeManager manager, CheckboxTreeViewer treeViewer) { this.treeViewer = treeViewer; manager.addListener(this); } public void checkChanged(TreeItem changedItem) { treeViewer.update(changedItem, null); } } /** * Implementation of {@link TreeManager}CheckListener for use in a * {@link CheckboxTableViewer}. */ public static class ModelListenerForCheckboxTable implements CheckListener { private CheckboxTableViewer tableViewer; public ModelListenerForCheckboxTable(TreeManager manager, CheckboxTableViewer tableViewer) { this.tableViewer = tableViewer; manager.addListener(this); } public void checkChanged(TreeItem changedItem) { tableViewer.update(changedItem, null); } } public static class ViewerCheckStateListener implements ICheckStateListener { public void checkStateChanged(CheckStateChangedEvent event) { Object checked = event.getElement(); if(checked instanceof TreeItem) { ((TreeItem)checked).setChangedByUser(true); ((TreeItem)checked).setCheckState(event.getChecked()); } } } /** * An {@link ICheckStateProvider} which properly provides checkbox state on * {@link TreeManager.TreeItem}s. */ public static class CheckStateProvider implements ICheckStateProvider { public boolean isChecked(Object element) { return ((TreeItem)element).checkState != CHECKSTATE_UNCHECKED; } public boolean isGrayed(Object element) { return ((TreeItem)element).checkState == CHECKSTATE_GRAY; } } /** * A {@link IBaseLabelProvider} for {@link TreeManager.TreeItem}s. */ public static class TreeItemLabelProvider extends LabelProvider { public String getText(Object element) { return ((TreeItem)element).getLabel(); } public Image getImage(Object element) { return ((TreeItem)element).getImage(); } } /** * An {@link ITreeContentProvider} for {@link TreeManager.TreeItem}s - will completely build the * tree structure represented by {@link TreeManager.TreeItem}s. */ public static class TreeItemContentProvider implements ITreeContentProvider { public Object[] getChildren(Object parentElement) { return ((TreeItem)parentElement).getChildren().toArray(); } public Object getParent(Object element) { return ((TreeItem)element).getParent(); } public boolean hasChildren(Object element) { return getChildren(element).length > 0; } public Object[] getElements(Object inputElement) { return getChildren(inputElement); } public void dispose() {} public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {} } /** * @return an {@link ICheckStateProvider} which will operate on * {@link TreeItem}s */ public static ICheckStateProvider getCheckStateProvider() { if(checkStateProvider == null) { checkStateProvider = new CheckStateProvider(); } return checkStateProvider; } /** * @return an {@link IBaseLabelProvider} which will provide the labels and * images of {@link TreeItem}s */ public static IBaseLabelProvider getLabelProvider() { if(labelProvider == null) { labelProvider = new TreeItemLabelProvider(); } return labelProvider; } /** * @return an {@link ITreeContentProvider} which will provide * {@link TreeItem} content in tree format. */ public static ITreeContentProvider getTreeContentProvider() { if(treeContentProvider == null) treeContentProvider = new TreeItemContentProvider(); return treeContentProvider; } /** * @return an {@link ICheckStateListener} which will respond to * {@link CheckStateChangedEvent}s by updating the model to reflect * them */ public ICheckStateListener getViewerCheckStateListener() { if(viewerCheckListener == null) viewerCheckListener = new ViewerCheckStateListener(); return viewerCheckListener; } /** * A single item in a tree of managed checkbox states. */ public class TreeItem { private String label; private ImageDescriptor imageDescriptor; private Image image; private TreeItem parent; private List children; private int checkState; private boolean changedByUser = false; public TreeItem(String label) { this.label = label; this.children = new ArrayList(); } public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } public Image getImage() { if(image == null) { if(imageDescriptor == null) { return null; } image = resourceManager.createImage(imageDescriptor); } return image; } public void setImageDescriptor(ImageDescriptor imageDescriptor) { this.imageDescriptor = imageDescriptor; } public void addChild(TreeItem newChild) { newChild.parent = this; children.add(newChild); synchParents(newChild); } public List getChildren() { return children; } public TreeItem getParent() { return parent; } /** * An internal call that forwards the change events but does <b>not</b> * cause any iterative synchronization to take place. * * @param newState */ private void internalSetCheckState(int newState) { if (newState == checkState) return; checkState = newState; fireListeners(this); } /** * External call to explicitly set the particular state of a {@link TreeManager.TreeItem}. * This is usually a response to an SWT check changed event generated by a Tree/Table. * * @param checked */ public void setCheckState(boolean checked) { int newState = checked ? CHECKSTATE_CHECKED : CHECKSTATE_UNCHECKED; if (checkState == newState) return; // Actually set the state and fire the CheckChangeEvent internalSetCheckState(newState); // Enforce the SWT rules for checked/gray behavior synchChildren(this); synchParents(this); } /** * From the client's perspective the state is a boolean. * * @return <code>true</code> if the state is not UNCHECKED */ public boolean getState() { return !(checkState == CHECKSTATE_UNCHECKED); } int getCheckState() { return checkState; } /** * If the new state is not "GRAY" then force all children to match that * state (recursively). * * @param changedItem */ private void synchChildren(TreeItem changedItem) { int newState = changedItem.checkState; // if the new state is 'GRAY' if (newState != CHECKSTATE_GRAY) { for (Iterator iterator = changedItem.children.iterator(); iterator .hasNext();) { TreeItem curItem = (TreeItem) iterator.next(); curItem.internalSetCheckState(newState); curItem.setChangedByUser(changedItem.isChangedByUser()); synchChildren(curItem); } } } /** * Set the parent's state based on the aggregate state of its children * using the following rules: * <ul> * <li>All children checked...parent checked</li> * <li>All children unchecked...parent unchecked</li> * <li>else...parent GRAY</li> * </ul> * * @param changedItem */ private void synchParents(TreeItem changedItem) { if(changedItem.parent == null) return; int newState = changedItem.checkState; if (newState == CHECKSTATE_GRAY) { // if the new state is 'GRAY' then -ALL- the parents are gray while (changedItem.parent != null && changedItem.parent.checkState != CHECKSTATE_GRAY) { changedItem.parent.internalSetCheckState(CHECKSTATE_GRAY); changedItem = changedItem.parent; } } else { // compute the parent's state - checked if all children are // checked, unchecked if all children are unchecked, gray if // some of each boolean checkedFound = newState == CHECKSTATE_CHECKED; boolean uncheckedFound = newState == CHECKSTATE_UNCHECKED; for (Iterator i = changedItem.parent.children.iterator(); i.hasNext() && (!checkedFound || !uncheckedFound);) { TreeItem item = (TreeItem) i.next(); switch(item.checkState) { case CHECKSTATE_CHECKED: { checkedFound = true; break; } case CHECKSTATE_GRAY: { checkedFound = uncheckedFound = true; break; } case CHECKSTATE_UNCHECKED: { uncheckedFound = true; break; }} } int oldState = changedItem.parent.checkState; if(checkedFound && uncheckedFound) { changedItem.parent.internalSetCheckState(CHECKSTATE_GRAY); } else if (checkedFound) { changedItem.parent.internalSetCheckState(CHECKSTATE_CHECKED); } else { changedItem.parent.internalSetCheckState(CHECKSTATE_UNCHECKED); } if(oldState != changedItem.parent.checkState) { synchParents(changedItem.parent); } } } /** * @param changedByUser The changedByUser to set. */ public void setChangedByUser(boolean changedByUser) { this.changedByUser = changedByUser; } /** * @return Returns the changedByUser. */ public boolean isChangedByUser() { return changedByUser; } } /** * Creates a new {@link TreeManager}. */ public TreeManager() { listeners = new ArrayList(); } /** * Add a {@link CheckListener} whose {@link CheckListener#checkChanged(TreeManager.TreeItem)} * method will be invoked when a {@link TreeItem} created in this {@link TreeManager} has a * check state change. * * @param listener */ public void addListener(CheckListener listener) { listeners.add(listener); } /** * Provides a {@link CheckListener} which updates a viewer whenever the * {@link TreeManager} model changes. * @param viewer The viewer whose check states will be appropriately * updated on a change to the model. * @return The created {@link CheckListener}. */ public CheckListener getCheckListener(ICheckable viewer) { if(viewer instanceof CheckboxTreeViewer) return new ModelListenerForCheckboxTree(this, (CheckboxTreeViewer)viewer); if(viewer instanceof CheckboxTableViewer) return new ModelListenerForCheckboxTable(this, (CheckboxTableViewer)viewer); return null; } /** * Sets up this {@link TreeManager} for standard interaction with the * provided {@link CheckboxTreeViewer}. In particular: * <ul> * <li>Adds a Label Provider for {@link TreeItem}s which provides both * labels and images.</li> * <li>Adds a {@link CheckStateProvider} for {@link TreeItem}s.</li> * <li>Adds an {@link IContentProvider} to build a tree from input * {@link TreeItem}s.</li> * <li>Adds an {@link ICheckStateListener} to the viewer to update the * appropriate {@link TreeItem}s upon a check box state change in the * viewer.</li> * <li>Adds a {@link CheckListener} to the {@link TreeManager} which will * automatically update the viewer on a {@link TreeItem} check state * change.</li> * </ul> * @param viewer the viewer to configure with this TreeManager. */ public void attachAll(CheckboxTreeViewer viewer) { viewer.setLabelProvider(getLabelProvider()); viewer.setCheckStateProvider(getCheckStateProvider()); viewer.setContentProvider(getTreeContentProvider()); viewer.addCheckStateListener(getViewerCheckStateListener()); getCheckListener(viewer); } /** * Sets up this {@link TreeManager} for standard interaction with the * provided {@link CheckboxTableViewer}. In particular: * <ul> * <li>Adds a Label Provider for {@link TreeItem}s which provides both * labels and images.</li> * <li>Adds a {@link CheckStateProvider} for {@link TreeItem}s.</li> * <li>Adds an {@link IContentProvider} to build a tree from input * {@link TreeItem}s.</li> * <li>Adds an {@link ICheckStateListener} to the viewer to update the * appropriate {@link TreeItem}s upon a check box state change in the * viewer.</li> * <li>Adds a {@link CheckListener} to the {@link TreeManager} which will * automatically update the viewer on a {@link TreeItem} check state * change.</li> * </ul> * @param viewer the viewer to configure with this TreeManager. */ public void attachAll(CheckboxTableViewer viewer) { viewer.setLabelProvider(getLabelProvider()); viewer.setCheckStateProvider(getCheckStateProvider()); viewer.setContentProvider(getTreeContentProvider()); viewer.addCheckStateListener(getViewerCheckStateListener()); getCheckListener(viewer); } /** * Dissociates a listener. * @param listener The listener to remove. */ public void removeListener(CheckListener listener) { listeners.remove(listener); } /** * Fires all listeners. * @param item The {@link TreeItem} that changed. */ private void fireListeners(TreeItem item) { for (Iterator i = listeners.iterator(); i.hasNext();) { CheckListener listener = (CheckListener) i.next(); listener.checkChanged(item); } } public void dispose() { resourceManager.dispose(); resourceManager = null; listeners.clear(); listeners = null; } }